#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <ctype.h>

#define NUM 1000
#define MAX_OPT_NUM 30 // 命令最多有30个选项

#define NONE_REDIR 0 // 非重定向
#define INPUT_REDIR 1 // 输入重定向
#define OUTPUT_REDIR 2 // 输出重定向
#define APPEND_REDIR 3 // 追加重定向

#define trimSpace(start) do{\
        while(isspace(*start)) ++start;\
    }while(0)// 只需执行一次，且这里不加';'

// 存储获取的命令行参数
char MyLineCommand[NUM];
// 存储命令的选项
char* MyArgv[MAX_OPT_NUM];

int latest_exit_code = 0;// 退出码
int latest_exit_signal = 0;// 终止信号

int redirType = NONE_REDIR;// 默认非重定向
char* redirFile = NULL;// 重定向到的文件，默认没有

// 分割重定向的命令字符串
void splitRedirect(char* commandStr)
{
    assert(commandStr);
    // 左闭右开
    char* start = commandStr;
    char* end = commandStr + strlen(commandStr);
    
    while(start < end)
    {
        if(*start == '>')
        {
            // 输出重定向
            *start = '\0';
            ++start;
            if(*start == '>')
            {
                // 追加重定向
                *start = '\0';
                ++start;
                // 设置重定向信息
                redirType = APPEND_REDIR;
            }
            else
            {
                redirType = OUTPUT_REDIR;
            }
            
            trimSpace(start);
            redirFile = start;
            
            break;
        }
        else if(*start == '<')
        {
            // 输入重定向
            *start = '\0';
            ++start;
            trimSpace(start);// 过滤空格
            // 设置重定向信息
            redirType = INPUT_REDIR;
            redirFile = start;

            break;
        }
        else
        {
            ++start;
        }
    }
}

int main()
{
    while(1)
    {
        // 消除过期的重定向
        redirType = NONE_REDIR;
        redirFile = NULL;
        errno = 0;

        // 输出提示符
        printf("[用户名@主机名 当前路径]# ");
        fflush(stdout);
        // 获取用户输入的命令行参数
        char* s = fgets(MyLineCommand, sizeof(MyLineCommand)-1, stdin);
        // 输入必须合法
        assert(s != NULL);

        // 输入时，会读取到\n，所以最好清除掉
        MyLineCommand[strlen(MyLineCommand) - 1] = 0;
        // printf("test:%s\n", s); // 测试

        // 将字符串（含有重定向），如"ls -a -l > text.txt"拆分为"ls -a -l"和"text.txt"两部分
        splitRedirect(MyLineCommand);
        // 将字符串变为多个子串 如："ls -a -l" -> "ls" "-a" "-l"
        MyArgv[0] = strtok(MyLineCommand, " ");
        // 为ls增加配色
        int i = 1;
        if(MyArgv[0] != NULL && strcmp(MyArgv[0], "ls") == 0)
        {
            MyArgv[i++] = (char*)"--color=auto";
        }
         
        // 若没有子串了，strtok会返回NULL，恰好MyArgv必须以NULL结尾，所以：
        while(MyArgv[i++] = strtok(NULL, " "));

        // 设置cd
        if(MyArgv[0] != NULL && strcmp(MyArgv[0], "cd") == 0)
        {
            // 对于cd命令，不用创建子进程，而是让shell执行对应的命令，本质就是执行系统接口
            // 这种不用子进程执行，而是shell自己执行的命令，就是内建（内置）命令。如cd
            if(MyArgv[1] != NULL)
                chdir(MyArgv[1]);

            continue;
        }

        // 设置echo
        if(MyArgv[0] != NULL && MyArgv[1] != 0 && strcmp(MyArgv[0], "echo") == 0)
        {
            if(strcmp(MyArgv[1], "$?") == 0)
            {
                printf("exit_code:%d, exit_signal:%d\n", latest_exit_code, latest_exit_signal);
            }
            else
            {
                printf("%s\n", MyArgv[1]);
            }
            continue;
        }
        // 测试 条件编译
#ifdef DEBUG
        for(int i = 0; MyArgv[i]; ++i)
        {
            printf("MyArgv[%d]:%s\n", i, MyArgv[i]);
        }
#endif
        // 执行命令 -- 让子进程执行
        pid_t id = fork();
        assert(id != -1);

        if(id == 0)
        {
            // child process
            
            // 命令由子进程执行，所以实际上重定向的工作一定是由子进程来完成的
            // 但是，怎样实现重定向，是由父进程来提供信息。（前面已经准备完毕）
            // 子进程的重定向，不会影响父进程！-> 进程具有独立性
            switch(redirType)
            {
                // 实现重定向
                case NONE_REDIR:
                    // 什么都不做
                    break;
                case INPUT_REDIR:
                    {
                        int fd = open(redirFile, O_RDONLY);
                        if(fd == -1)
                        {
                            perror("open");
                            exit(errno);
                        }
                        // 设置重定向
                        dup2(fd, 0);
                    }
                    break;
                case OUTPUT_REDIR:
                case APPEND_REDIR:
                    {
                        umask(0);
                        int flags = O_WRONLY | O_CREAT;
                        if(redirType == APPEND_REDIR)
                            flags |= O_APPEND;
                        else
                            flags |= O_TRUNC;

                        int fd = open(redirFile, flags, 0666);
                        if(fd == -1)
                        {
                            perror("open");
                            exit(errno);
                        }
                        // 设置重定向
                        dup2(fd, 1);
                    }
                    break;
                default:
                    printf("error:redirect\n");
                    break;
            }

            // 执行程序替换的时候，会不会影响曾将打开的重定向的文件？ -> 不会
            // 程序替换替换的是代码和数据，但进程对应的PCB等内核数据结构是不变的
            execvp(MyArgv[0], MyArgv);
            // 只要execvp返回，就代表调用失败
            exit(errno);
        }

        int status = 0;
        pid_t ret_id = waitpid(id, &status, 0);
        assert(ret_id > 0);
        (void)ret_id;
        latest_exit_code = (status >> 8) & 0xff;
        latest_exit_signal = status & 0x7f;
    }


    return 0;
}

